package com.yzl.frelink.service.matlab;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONObject;

import com.yzl.frelink.entities.chart.Function;
import com.yzl.frelink.entities.chart.StateTree;
import com.yzl.frelink.entities.chart.StateTreeNode;
import com.yzl.frelink.entities.chart.Transition;
import com.yzl.frelink.entities.component.Chart;
import com.yzl.frelink.entities.component.Component;
import com.yzl.frelink.entities.model.Model;
import com.yzl.frelink.util.JsonReader;

public class DotCompiler {
    public static void printModelDot(Model model) {
        List<Chart> charts = model.getCharts();
        for (Chart chart : charts) {
            String dotScript = getChartDot(chart);
            try {
                File dotFile = new File("./public/out_dot/" + model.getName() + "_" + chart.getName() + ".dot");
                Writer writer = new OutputStreamWriter(new FileOutputStream(dotFile), "UTF-8");
                writer.write(dotScript);
                writer.close();
            } catch (Exception e) {
                System.out.println("Error writing file: " + e.getMessage());
            }
        }
    }

    private static String getChartDot(Chart chart) {
        List<String> functions = new ArrayList<>();
        for (Function func : chart.getFunctions()) {
            functions.add(func.getName() + " [label=\"" + func.getName() + "\"];");
        }
        String subgraph = DotCompiler.generateSubgraph(chart.getStateTree());
        String link = DotCompiler.generateLine(chart.getTransitions());
        StringBuilder sb = new StringBuilder();
        sb.append("digraph ").append(chart.getName()).append(" {\n");
        sb.append("rankdir=LR;\n");
        sb.append("compound=true;\n");
        sb.append("node [shape=box];\n");
        sb.append(subgraph);
        sb.append(link);
        for (String func : functions) {
            sb.append(func).append("\n");
        }
        sb.append("}\n");
        // System.out.println(sb.toString());
        return sb.toString();
    }


    private static String generateSubgraph(Object state) {
        if (state == null) return "";
        List<StateTreeNode> leafStates = new ArrayList<>();
        List<StateTreeNode> subgraphs = new ArrayList<>();
        StringBuilder sb = new StringBuilder();

        if (state instanceof StateTree) {
            StateTree tree = (StateTree) state;
            for (StateTreeNode child : tree.getChildren()) {
                if (child.getChildren().isEmpty()) {
                    leafStates.add(child);
                } else {
                    subgraphs.add(child);
                }
            }
            for (StateTreeNode leaf : leafStates) {
                sb.append(leaf.getName()).append(" [label=\"").append(leaf.getLabel()).append("\"");
                if (leaf.getType() == StateTreeNode.START || leaf.getType() == StateTreeNode.JUNCTION) sb.append(", shape=point");
                sb.append("];\n");
            }
            for (StateTreeNode subgraph : subgraphs) {
                sb.append(generateSubgraph(subgraph)).append("\n");
            }
            return sb.toString();
        } else {
            StateTreeNode node = (StateTreeNode) state;
            for (StateTreeNode child : node.getChildren()) {
                if (child.getChildren().isEmpty()) {
                    leafStates.add(child);
                } else {
                    subgraphs.add(child);
                }
            }
            sb.append("subgraph cluster_").append(node.getName()).append(" {\n");
            sb.append("label = \"").append(node.getLabel()).append("\";\n");
            sb.append("internal_").append(node.getName()).append(" [style=invis,shape=point, width=0, height=0];\n");
            for (StateTreeNode leaf : leafStates) {
                sb.append(leaf.getName()).append(" [label=\"").append(leaf.getLabel()).append("\"");
                if (leaf.getType() == StateTreeNode.START || leaf.getType() == StateTreeNode.JUNCTION) sb.append(", shape=point");
                sb.append("];\n");
            }
            for (StateTreeNode subgraph : subgraphs) {
                sb.append(generateSubgraph(subgraph)).append("\n");
            }
            sb.append("}");
            return sb.toString();
        }
    }

    private static String generateLine(List<Transition> transitions) {
        StringBuilder sb = new StringBuilder();
        for (Transition transition : transitions) {
            boolean checkFrom = transition.getFrom().getChildren().size() != 0;
            boolean checkTo = transition.getTo().getChildren().size() != 0;
            if (!checkFrom && !checkTo) {
                sb.append(transition.getFrom().getName()).append(" -> ").append(transition.getTo().getName());
            } else if (checkFrom && !checkTo) {
                sb.append("internal_").append(transition.getFrom().getName()).append(" -> ").append(transition.getTo().getName()).append("[ltail = cluster_").append(transition.getFrom().getName()).append("];");
            } else if (!checkFrom && checkTo) {
                sb.append(transition.getFrom().getName()).append(" -> ").append("internal_").append(transition.getTo().getName()).append("[lhead=cluster_").append(transition.getTo().getName()).append("];");
            } else {
                sb.append("internal_").append(transition.getFrom().getName()).append(" -> ").append("internal_").append(transition.getTo().getName()).append("[ltail=cluster_").append(transition.getFrom().getName()).append(", lhead=cluster_").append(transition.getTo().getName()).append("];");
            }
            sb.append("\n"); 
        }
        return sb.toString();
    }

    public static void setModelPosition(Model model) {
        List<Chart> charts = model.getCharts();
        for (Chart chart : charts) {
            setChartPosition(model, chart);
        }
    }

    private static void setChartPosition(Model model,  Chart chart) {
        // Set the position of the chart's state tree
        try {
            ProcessBuilder pb = new ProcessBuilder("dot", "-Tjson0", "./public/out_dot/" + model.getName() + "_" + chart.getName() + ".dot");
            pb.redirectOutput(ProcessBuilder.Redirect.to(new File("./public/out_dot/" + model.getName() + "_" + chart.getName() + ".json")));
            Process p = pb.start();
            p.waitFor();
            JSONObject dot = JsonReader.readJson("./public/out_dot/" + model.getName() + "_" + chart.getName() + ".json");
            Map<String, int[]> positions = DotCompiler.getPositions(dot);
            for (Map.Entry<String, int[]> entry : positions.entrySet()) {
                StateTreeNode node = chart.getState(entry.getKey());
                if (node != null) {
                    node.setPosition(entry.getValue());
                }
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("Error running dot command: " + e.getMessage());
        }
    }

    private static Map<String, int[]> getPositions(JSONObject dot) {
        Map<String, int[]> positions = new HashMap<>();
        float bb_height = Float.parseFloat(dot.getString("bb").split(",")[3]);
        JSONArray states = dot.getJSONArray("objects");
        JSONArray links = dot.getJSONArray("edges");
        for (int i = 0; i < states.length(); i++) {
            JSONObject state = states.getJSONObject(i);
            String name = state.getString("name");
            positions.put(name.replace("cluster_", ""), getPosition(state, bb_height, 72));
        }
        return positions;
    }

    private static int[] getPosition(JSONObject state, float bb_height, int ruler) {
        // dot from left bottom in inch
        // stateflow from left top
        // 1 inch = 72 points
        if (state.has("bb")) {
            String[] bb = state.getString("bb").split(",");
            float height = (Float.parseFloat(bb[3]) - Float.parseFloat(bb[1]));
            float top = bb_height-Float.parseFloat(bb[3]);
            float width = (Float.parseFloat(bb[2]) - Float.parseFloat(bb[0]));
            float left = Float.parseFloat(bb[0]);

            return remainOne(new float[]{left, top, width, height});
        } else if (state.has("pos") && state.has("height") && state.has("width")) {
            String[] pos = state.getString("pos").split(",");
            float left = (Float.parseFloat(pos[0]) - (Float.parseFloat(state.getString("width")) * ruler / 2));
            float top = bb_height-(Float.parseFloat(pos[1]) + (Float.parseFloat(state.getString("height")) * ruler / 2));
            float width = (Float.parseFloat(state.getString("width")) * ruler);
            float height = (Float.parseFloat(state.getString("height")) * ruler);
            return remainOne(new float[]{left, top, width, height});
        } else {
            System.out.println("No position data");
            return new int[]{};
        }
    }

    private static int[] remainOne(float[] pos) {
        int[] res = new int[pos.length];
        for (int i = 0; i < pos.length; i++) {
            res[i] = Math.round(pos[i] * 10) / 10;
        }
        return res;
    }
}
